Micron Document
🎖️GitЯра🎖️

Commit 65e937427f30972788026b33aecf831a881f0046


Parents : 21b6540
Author : James Rich <2199651+jamesarich@users.noreply.github.com>
Signature : Signature validation error
Date : 2026-06-09T11:28:37-05:00
Committer : GitHub <noreply@github.com>
Date : 2026-06-09T16:28:37Z

fix(mqtt): make the MQTT client-id unique per connection (#5755)

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Changes
Diff

diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MqttManagerImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MqttManagerImpl.kt
index 5d24a9aaf4..f22693614f 100644
--- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MqttManagerImpl.kt
+++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MqttManagerImpl.kt
@@ -45,6 +45,7 @@ import org.meshtastic.mqtt.ProbeResult
import org.meshtastic.mqtt.probe
import org.meshtastic.proto.MqttClientProxyMessage
import org.meshtastic.proto.ToRadio
+import kotlin.uuid.Uuid
@Single
class MqttManagerImpl(
@@ -132,8 +133,10 @@ class MqttManagerImpl(
val endpoint = resolveEndpoint(address, tlsEnabled)
val result =
MqttClient.probe(endpoint = endpoint) {
- // Use node ID for consistent client identification across platforms
- clientId = "MeshtasticAndroidMqttProbe-${nodeRepository.myId.value ?: "unknown"}"
+ // Per-connection random suffix: myId identifies the node (and is null →
+ // "unknown" before the node record loads), so two probes can collide on one
+ // client-id and evict each other (SESSION_TAKEN_OVER). See MQTTRepositoryImpl.
+ clientId = "MeshtasticAndroidMqttProbe-${nodeRepository.myId.value ?: "unknown"}-${Uuid.random()}"
val user = username?.takeUnless { it.isEmpty() }
val pass = password?.takeUnless { it.isEmpty() }
if (user != null) this.username = user

diff --git a/core/network/src/commonMain/kotlin/org/meshtastic/core/network/repository/MQTTRepositoryImpl.kt b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/repository/MQTTRepositoryImpl.kt
index b33470c6ca..eb2511b403 100644
--- a/core/network/src/commonMain/kotlin/org/meshtastic/core/network/repository/MQTTRepositoryImpl.kt
+++ b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/repository/MQTTRepositoryImpl.kt
@@ -54,6 +54,7 @@ import org.meshtastic.mqtt.packet.Subscription
import org.meshtastic.proto.ModuleConfig
import org.meshtastic.proto.MqttClientProxyMessage
import kotlin.concurrent.Volatile
+import kotlin.uuid.Uuid
@Single(binds = [MQTTRepository::class])
class MQTTRepositoryImpl(
@@ -112,7 +113,12 @@ class MQTTRepositoryImpl(
@OptIn(ExperimentalSerializationApi::class)
override val proxyMessageFlow: Flow<MqttClientProxyMessage> = callbackFlow {
- val ownerId = "MeshtasticAndroidMqttProxy-${nodeRepository.myId.value ?: "unknown"}"
+ // Append a per-connection random id. myId identifies the *node* (and is null →
+ // "unknown" before the local node record loads), so it is not unique per client:
+ // clients sharing a node id — and the whole "unknown" pool on the public broker —
+ // evict each other (SESSION_TAKEN_OVER), storming reconnects. A fresh id per client
+ // (captured for its lifetime, so auto-reconnects stay stable) prevents that.
+ val ownerId = "MeshtasticAndroidMqttProxy-${nodeRepository.myId.value ?: "unknown"}-${Uuid.random()}"
val channelSet = radioConfigRepository.channelSetFlow.first()
val mqttConfig = radioConfigRepository.moduleConfigFlow.first().mqtt

diff --git a/core/network/src/commonTest/kotlin/org/meshtastic/core/network/repository/MQTTRepositoryImplTest.kt b/core/network/src/commonTest/kotlin/org/meshtastic/core/network/repository/MQTTRepositoryImplTest.kt
index 95dc6fa4fb..ce6ebe3f89 100644
--- a/core/network/src/commonTest/kotlin/org/meshtastic/core/network/repository/MQTTRepositoryImplTest.kt
+++ b/core/network/src/commonTest/kotlin/org/meshtastic/core/network/repository/MQTTRepositoryImplTest.kt
@@ -256,7 +256,11 @@ class MQTTRepositoryImplTest {
subscriptions.map { it.topicFilter },
)
assertTrue(subscriptions.all { it.maxQos == QoS.AT_LEAST_ONCE && it.noLocal })
- assertEquals("MeshtasticAndroidMqttProxy-!12345678", harness.setups.single().ownerId)
+ assertTrue(
+ harness.setups.single().ownerId.startsWith("MeshtasticAndroidMqttProxy-!12345678-"),
+ "ownerId should be the node-scoped prefix plus a unique per-connection suffix, " +
+ "was: ${harness.setups.single().ownerId}",
+ )
assertEquals(MqttLogLevel.DEBUG, harness.setups.single().logLevel)
collector.cancelAndJoin()

Served by rngit 1.5.2 - Generated in 0.08s